﻿using System;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.ServiceModel;
using System.ServiceModel.Configuration;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using OpenZWaveDotNet;

namespace Home.ZWave
{
    public class HomeManager
    {
        private static HomeManager instance;

        private ZWManager manager = null;
        private ServiceHost selfHost = null;
        private WebServer webServer = null;
        private Dictionary<System.Guid, ArrayList> notifications = new Dictionary<Guid, ArrayList>();
        private Dictionary<System.Guid, DateTime> clientLastAcess = new Dictionary<Guid, DateTime>();

        public const string comPortSettingKey = "comPort";
        public const string baseAddressSettingKey = "baseAddress";
        public const string clientNotificationTimeoutKey = "clientNotificationTimeout";
        public const string serviceNameKey = "serviceName";
        public const string displayBrowserAtStartupKey = "displayBrowserAtStartup";

        public const string interfacePrefix = "m";
        public const string servicePrefix = "s";

        public static HomeManager Instance
        {
            get
            {
                if (instance == null)
                {
                    instance = new HomeManager();
                }

                return instance;
            }
        }

        public HomeConnection GetHomeContext()
        {
            return new HomeConnection(
                    new System.Data.EntityClient.EntityConnection(
                        String.Format(
                            "metadata=res://*/Home.csdl|res://*/Home.ssdl|res://*/Home.msl;provider=System.Data.SqlServerCe.3.5;provider connection string=\"Data Source={0}\"",
                            new FileInfo(System.Reflection.Assembly.GetEntryAssembly().Location).Directory.ToString() + "\\db\\Home.sdf")));
        }

        private void RemoveOldClients()
        {
            System.Guid[] keys = this.clientLastAcess.Keys.ToArray();

            for (int i = 0; i < keys.Length; i++)
            {
                System.Guid clGuid = keys[i];

                int clientNotificationTimeout = 150;

                int.TryParse(GetSettingValue(clientNotificationTimeoutKey), out clientNotificationTimeout);

                if ((DateTime.Now - this.clientLastAcess[clGuid]).Seconds > clientNotificationTimeout)
                {
                    this.clientLastAcess.Remove(clGuid);
                    this.notifications.Remove(clGuid);
                    break;
                }
            }
        }

        private void AddNotification(object notification)
        {
            RemoveOldClients();

            foreach (System.Guid clGuid in this.notifications.Keys)
            {
                this.notifications[clGuid].Add(notification);
            }
        }

        public object[] GetNotifications(System.Guid clientGuid)
        {
            RemoveOldClients();

            if (!notifications.ContainsKey(clientGuid))
            {
                notifications.Add(clientGuid, new ArrayList());
                if (!clientLastAcess.ContainsKey(clientGuid)) clientLastAcess.Add(clientGuid, DateTime.Now);
            }

            clientLastAcess[clientGuid] = DateTime.Now;
            object[] clientNotifications = notifications[clientGuid].ToArray();
            notifications[clientGuid].Clear();
            return clientNotifications;
        }

        public string GetTranslatedWidgetValue(System.Guid widgetGuid, string valueVal)
        {
            HomeConnection homeContext = GetHomeContext();

            var translation = homeContext.WidgetValues.FirstOrDefault(
                c => c.WidgetGuid == widgetGuid && c.ValueVal == valueVal);

            if (translation == null) // no exact match
            {
                double bestVal = double.MinValue;
                string bestWidgetVal = null;
                double valval;

                if (!double.TryParse(valueVal, out valval)) return null;

                var widget = homeContext.Widgets.FirstOrDefault(
                    w => w.WidgetGuid == widgetGuid);

                widget.ValueTranslations.Load();

                foreach (WidgetValue wv in widget.ValueTranslations)
                {
                    double wvalval;
                    if (!double.TryParse(wv.ValueVal, out wvalval)) continue;

                    if (Math.Abs(wvalval - valval) < Math.Abs(wvalval - bestVal))
                    {
                        bestVal = wvalval;
                        bestWidgetVal = wv.WidgetVal;
                    }
                }

                return bestWidgetVal;
            }

            return translation.WidgetVal;
        }

        public string GetTranslatedValueValue(System.Guid widgetGuid, string widgetVal)
        {
            HomeConnection homeContext = GetHomeContext();

            var translation = homeContext.WidgetValues.FirstOrDefault(
                c => c.WidgetGuid == widgetGuid && c.WidgetVal == widgetVal);

            return translation.ValueVal;
        }

        public Device GetDevice(int homeId, short nodeId)
        {
            HomeConnection homeContext = GetHomeContext();

            var device = homeContext.Devices.FirstOrDefault(
                c => c.HomeId == homeId &&
                c.NodeId == nodeId);

            return device;
        }

        public string GetSettingValue(string key)
        {
            HomeConnection homeContext = GetHomeContext();

            var setting = homeContext.Settings.FirstOrDefault(s => s.Key == key);

            if (setting == null) return string.Empty;

            return setting.Value;
        }

        public bool SetSettingValue(string key, string value)
        {
            HomeConnection homeContext = GetHomeContext();

            var setting = homeContext.Settings.FirstOrDefault(s => s.Key == key);

            if (setting == null) return false;

            setting.Value = value;

            homeContext.SaveChanges(true);

            return true;
        }

        public Value GetValue(int homeId, short nodeId, int commandClassId, int instance, int index)
        {
            HomeConnection homeContext = GetHomeContext();

            var value = homeContext.Values.FirstOrDefault(
                nv => nv.NodeId == nodeId
                        && nv.HomeId == homeId
                        && nv.CommandClassId == commandClassId
                        && nv.Instance == instance
                        && nv.Index == index);

            return value;
        }

        public bool Start()
        {
            string baseAddress = GetSettingValue(baseAddressSettingKey);

            if (!baseAddress.EndsWith("/")) baseAddress += "/";

            if (webServer == null)
            {
                try
                {
                    webServer = new WebServer(new Uri(baseAddress + interfacePrefix + "/"));

                    webServer.Start();
                }
                catch
                {
                    Console.WriteLine(string.Format("Unable to start the server on address {0}. Please start the application providing a base address. e.g. Home.Server.exe http://localhost/ ", baseAddress));
                    return false;
                }

                Console.WriteLine("Web server started");
            }

            if (selfHost == null)
            {
                selfHost = new ServiceHost(typeof(HomeService), new Uri[] { new Uri(baseAddress + servicePrefix + "/") });

                ServiceEndpoint ep = selfHost.AddServiceEndpoint(typeof(HomeService), new WebHttpBinding(), "/");
                ep.Behaviors.Add(new WebHttpBehavior());

                selfHost.Open();

                Console.WriteLine("Service host started");
            }

            if (manager == null)
            {
                manager = new ZWManager();
                manager.Create(new FileInfo(System.Reflection.Assembly.GetEntryAssembly().Location).Directory.ToString() + "\\config", @"");
                manager.OnNotification += new ManagedNotificationsHandler(NotificationHandler);

                manager.AddDriver(@"\\.\" + GetSettingValue(comPortSettingKey));

                Console.WriteLine("ZWave manager started");
            }

            if (GetSettingValue(displayBrowserAtStartupKey).ToLower() == "true")
            {
                System.Diagnostics.Process.Start(baseAddress + interfacePrefix + "/");
            }

            return true;
        }

        public void Stop()
        {
            if (manager != null)
            {
                manager.Destroy();
                manager = null;
            }

            if (selfHost != null)
            {
                selfHost.Close();
                selfHost = null;
            }

            if (webServer != null)
            {
                webServer.Stop();
                webServer.Dispose();
            }
        }

        public string GetValue(Value val)
        {
            ZWValueID id = new ZWValueID((uint)val.HomeId, (byte)val.NodeId, (ZWValueID.ValueGenre)val.GenreId, (byte)val.CommandClassId, (byte)val.Instance, (byte)val.Index, (ZWValueID.ValueType)val.TypeId);

            string strVal = null;

            manager.GetValueAsString(id, out strVal);

            return strVal;
        }

        public bool SetValue(Value val, string newVal)
        {
            ZWValueID id = new ZWValueID((uint)val.HomeId, (byte)val.NodeId, (ZWValueID.ValueGenre)val.GenreId, (byte)val.CommandClassId, (byte)val.Instance, (byte)val.Index, (ZWValueID.ValueType)val.TypeId);

            return manager.SetValue(id, newVal);
        }

        public void NotificationHandler(ZWNotification notification)
        {
            HomeConnection homeContext = GetHomeContext();

            switch (notification.GetType())
            {
                case ZWNotification.Type.ValueAdded:
                    {
                        ZWValueID id = notification.GetValueID();

                        int homeId = (int)id.GetHomeId();
                        short nodeId = (short)id.GetNodeId();
                        int commandClassId = (int)id.GetCommandClassId();
                        int instance = (int)id.GetInstance();
                        int index = (int)id.GetIndex();

                        Value nodeValue = GetValue(homeId, nodeId, commandClassId, instance, index);

                        if (nodeValue == null)
                        {
                            nodeValue = new Value();
                            nodeValue.NodeId = nodeId;
                            nodeValue.HomeId = homeId;
                            nodeValue.CommandClassId = commandClassId;
                            nodeValue.Instance = instance;
                            nodeValue.Index = index;
                            nodeValue.GenreId = (short)id.GetGenre();
                            nodeValue.TypeId = (short)id.GetType();
                            nodeValue.Label = manager.GetValueLabel(id);
                            nodeValue.Help = manager.GetValueHelp(id);
                            nodeValue.Units = manager.GetValueUnits(id);
                            nodeValue.Device = GetDevice(homeId, nodeId);
                            nodeValue.ValueGuid = System.Guid.NewGuid();
                            nodeValue.Maximum = "99";

                            homeContext.AddToValues(nodeValue);
                        }

                        nodeValue.IsActive = true;
                        homeContext.SaveChanges();

                        break;
                    }
                case ZWNotification.Type.ValueRemoved:
                    {
                        ZWValueID id = notification.GetValueID();

                        int homeId = (int)id.GetHomeId();
                        short nodeId = (short)id.GetNodeId();
                        int commandClassId = (int)id.GetCommandClassId();
                        int instance = (int)id.GetInstance();
                        int index = (int)id.GetIndex();

                        Value nodeValue = GetValue(homeId, nodeId, commandClassId, instance, index);

                        if (nodeValue != null)
                        {
                            nodeValue.IsActive = false;
                            homeContext.SaveChanges();
                        }

                        break;
                    }
                case ZWNotification.Type.ValueChanged:
                    {
                        ZWValueID id = notification.GetValueID();

                        int homeId = (int)id.GetHomeId();
                        short nodeId = (short)id.GetNodeId();
                        int commandClassId = (int)id.GetCommandClassId();
                        int instance = (int)id.GetInstance();
                        int index = (int)id.GetIndex();

                        Value val = GetValue(homeId, nodeId, commandClassId, instance, index);

                        val.CurrentValue = GetValue(val);

                        homeContext.SaveChanges();

                        //Add a notification for each widget that refers this value
                        var query = from w in homeContext.Widgets where w.Value.ValueGuid == val.ValueGuid select w;

                        foreach (Widget w in query)
                        {
                            string currentValue = val.CurrentValue;

                            if (w.RequireValueTranslation)
                            {
                                currentValue = GetTranslatedWidgetValue(w.WidgetGuid, currentValue);
                            }

                            AddNotification(new { Type = "ValueChanged", WidgetTypeId = w.WidgetTypeId, WidgetGuid = w.WidgetGuid, Value = currentValue });
                        }

                        break;
                    }
                case ZWNotification.Type.Group:
                    {
                        break;
                    }
                case ZWNotification.Type.NodeAdded:
                    {
                        short nodeId = notification.GetNodeId();
                        int homeId = (int)notification.GetHomeId();

                        Device node = GetDevice(homeId, nodeId);

                        if (node == null)
                        {
                            node = new Device();
                            node.NodeId = nodeId;
                            node.HomeId = homeId;
                            node.DeviceGuid = System.Guid.NewGuid();

                            homeContext.AddToDevices(node);
                        }

                        node.IsActive = true;

                        homeContext.SaveChanges();

                        manager.EnablePoll((uint)homeId, (byte)nodeId);

                        break;
                    }
                case ZWNotification.Type.NodeRemoved:
                    {
                        short nodeId = notification.GetNodeId();
                        int homeId = (int)notification.GetHomeId();

                        Device device = GetDevice(homeId, nodeId);

                        if (device != null)
                        {
                            device.IsActive = false;

                            homeContext.SaveChanges();
                        }

                        break;
                    }
                case ZWNotification.Type.NodeProtocolInfo:
                    {
                        short nodeId = notification.GetNodeId();
                        int homeId = (int)notification.GetHomeId();

                        Device device = GetDevice(homeId, nodeId);

                        if (device != null)
                        {
                            device.Type = manager.GetNodeType((uint)homeId, (byte)nodeId);

                            if (device.Label == null)
                            {
                                device.Label = device.Type;
                            }

                            homeContext.SaveChanges();
                        }

                        break;
                    }
                case ZWNotification.Type.NodeNaming:
                    {
                        short nodeId = notification.GetNodeId();
                        int homeId = (int)notification.GetHomeId();

                        Device device = GetDevice(homeId, nodeId);

                        if (device != null)
                        {
                            device.Type = manager.GetNodeType((uint)homeId, (byte)nodeId);

                            device.Manufacturer = manager.GetNodeManufacturerName((uint)homeId, (byte)nodeId);
                            device.Product = manager.GetNodeProductName((uint)homeId, (byte)nodeId);

                            homeContext.SaveChanges();
                        }

                        break;
                    }
                case ZWNotification.Type.NodeStatus:
                    {
                        break;
                    }
                case ZWNotification.Type.PollingDisabled:
                    {
                        break;
                    }
                case ZWNotification.Type.PollingEnabled:
                    {
                        break;
                    }
                case ZWNotification.Type.DriverReady:
                    {
                        // Mark all the nodes inactive
                        var nodeQuery = from n in homeContext.Devices
                                        select n;

                        foreach (Device n in nodeQuery)
                        {
                            n.IsActive = false;
                        }

                        var nodeValueQuery = from nv in homeContext.Values select nv;

                        foreach (Value nv in nodeValueQuery)
                        {
                            nv.IsActive = false;
                        }

                        homeContext.SaveChanges();

                        break;
                    }
            }
        }
    }
}
